<?php
define('THUMBNAIL_ENLARGE_NONE', 0);
define('THUMBNAIL_ENLARGE_ALWAYS', 1);
define('TP_ALLOWED_IMG_TYPE', 'image/jpeg,image/pjpeg,image/gif,image/png,image/x-png');
define('IMAGEMAGICK_RESIZE_CMD', '%sconvert -quality %d -resize %dx%d "%s" "%s"');
define('IMAGEMAGICK_RESIZE_FILLED_CMD', '%sconvert "%s" -quality %d -resize %dx%d -size %dx%d xc:%s +swap -gravity center -composite "%s"');
define('IMAGEMAGICK_WATERMARK_CMD', '%sconvert %s -gravity southeast -chop %dx%d png:- | %scomposite -geometry %s%s -dissolve %s%% png:- "%s" "%s"');

require_once(dirname(__FILE__).'/tp_watermark.class.php');

class thumbnail_party {
	var $strVersion = '1.1.9';
	var $strSourceFile;
	var $intThumbnailWidth;
	var $intThumbnailHeight;
	var $strBgColor = NULL;
	var $strOutputType = 'jpg';
	var $intJPEGQuality = 85; // Picture quality for JPEG image. Max. value 100
	var $boolCache = TRUE; // use caching
	var $boolError = FALSE;
	var $strErrMsg = '';
    var $name = '';
	
	/**
	 * Constructor
	 *
	 * @param integer $intWidth Thumbnail width. Default 100.
	 * @param integer $intHeight Thumbnail height. Default 100.
	 * @param string $strColor Color string. Possible format : '#RRGGBB' & 'RRGGBB', Default NULL
	 * @param string $strType Thumbnail Type. Possible value : 'jpg', 'jpeg', 'gif' & 'png'. Default 'jpg'
	 * @param string $strCacheDir Cache path. Default NULL means using class default path 'cache/'
	 * @return void
	 * @access public
	 */
	function thumbnail_party ($intWidth=100, $intHeight=100, $strColor=NULL, $strType='jpg', $strCacheDir=NULL,$name = '') {
		
		$this->intThumbnailWidth = $intWidth;
		$this->intThumbnailHeight = $intHeight;
		$this->set_background($strColor);
		$this->set_output_type($strType);
        $this->name = $name;
		if (!empty($strCacheDir)) {
			$this->set_cache_path($strCacheDir);
		}
	}
	
	/**
	 * Return the version of Thumbnail Party
	 *
	 * @return string version string
	 * @access public
	 */
	function get_version () {
		return $this->strVersion;
	}
	
	/**
	 * To set cache path
	 *
	 * @param string $strDir Path to cache directory. 
	 * @return boolean true when the cache directory avaliable
	 * @access public
	 */
	function set_cache_path ($strDir) {
		$this->_clear_error();
		if (substr($strDir, -1, 1) != '/') $strDir .= '/';
		if (is_writable($strDir)) {
			$this->strCacheDir = $strDir;
			return TRUE;
		} else {
			return $this->_trigger_error('Cache directory is not present or unable to write');
		}
	}
	
	/**
	 * To set ImageMagick path
	 *
	 * @param string $strIMPath Path to ImageMagick. Usually are '/usr/bin/' or '/usr/local/bin/'. Set to NULL of using GDlib
	 * @return boolean true when the ImageMagick is avaliable
	 * @access public
	 */
	function set_imagemagick_path ($strIMPath = NULL) {
		$this->_clear_error();
		if (empty($strIMPath) || file_exists($strIMPath)) {
			// check submitted argument is a file or a directory for backward compatible.
			if (!is_file($strIMPath)) $this->strImageMagicPath = $strIMPath;
			elseif (is_dir($strIMPath)) $this->strImageMagicPath = dirname($strIMPath).'/';
			return TRUE;
		} else {
			return $this->_trigger_error('Invalid ImageMagick path');
		}
	}
	
	/**
	 * Assign source image path
	 *
	 * @param string $strSourceFile Path and filename of source image
	 * @return boolean true when the source is avaliable
	 * @access public
	 */
	function set_source_file ($strSourceFile) {
		$this->_clear_error();
                
		if (trim($strSourceFile) != '') {
                        
			$this->strSourceFile = $strSourceFile;
			return TRUE;
		} else {
			return $this->_trigger_error('Invalid source image ('.$strSourceFile.')');
		}
	}
	
	/**
	 * To set thumbnail type
	 *
	 * @param string $strOutType Possible value : 'jpg', 'jpeg', 'gif' and 'png'. Default 'jpg'
	 * @return boolean true when assigned value is correct.
	 * @access public
	 */
	function set_output_type($strOutputType = 'jpg') {
		$this->_clear_error();
		$strOutputType = strtolower($strOutputType);
		if ($strOutputType=='jpg' || $strOutputType=='jpeg' || $strOutputType=='gif' || $strOutputType=='png') {
			$this->strOutputType = $strOutputType;
			return TRUE;
		} else {
			return $this->_trigger_error('Invalid output image type');
		}
	}

	/**
	 * To set thumbnail side
	 *
	 * @param integer $intWidth Thumbnail width. Default 100px
	 * @param integer $intHeight Thumbnail height. Default 100px
	 * @return void
	 * @access public
	 */
	function set_thumbnail_size($intWidth = 100, $intHeight = 100) {
		$this->intThumbnailWidth = $intWidth;
		$this->intThumbnailHeight = $intHeight;
	}
	
	/**
	 * To set thumbnail background color
	 * If background color is set. The shorter side will be filled with this color 
	 * 
	 * @param string $strColor Color string. Possible format : '#RRGGBB' & 'RRGGBB', Default NULL
	 * @return boolean when the color string is incorrect
	 * @access public
	 */
	function set_background($strColor) {
		$this->_clear_error();
		if (empty($strColor)) $this->strBgColor = NULL;
		elseif (strlen($strColor)==6) $this->strBgColor = '#'.$strColor;
		elseif (strlen($strColor)==7 && substr($strColor, 0, 1)=='#') $this->strBgColor = $strColor;
		else {
			return $this->_trigger_error('Invalid color. (Format: #RRGGBB or RRGGBB)');
		}
		return TRUE;
	}
	
	/**
	 * Set Watermark file path
	 * Set to NULL for shutdown watermark feature
	 *
	 * @param string $strWatermarkPath path and filename to watermark image
	 * @param integer $intWatermarkPosition position of watermark image will be placed in
	 * @param integer $intWatermarkOpacity opacity of the watermark
	 * @return boolean true if watermark object built-up successfully
	 * @access public
	 */
	function set_watermark ($strWatermarkPath=NULL, $intWatermarkPosition=WATERMARK_POSITION_RANDOM, $intWatermarkOpacity=50) {
		$this->_clear_error();
		if ($strWatermarkPath == NULL) {
			$this->objWatermark = NULL;
			return TRUE;
		}
		$this->objWatermark =& new tp_watermark($strWatermarkPath);
		if (!$this->objWatermark->boolError) {
			$this->objWatermark->set_position((int)$intWatermarkPosition);
			$this->objWatermark->set_opacity((int)$intWatermarkOpacity);
			return TRUE;
		} else {
			$this->objWatermark = NULL;
			return $this->_trigger_error('Cannot obtain watermark object');
		}
	}
		
	/**
	 * To set the enlarge mode
	 * If enlarge mode set to THUMBNAIL_ENLARGE_NONE(0),
	 * source will not be enlarged when the source size smaller then output size,
	 * and all space will fill with background color
	 * If enlarge mode set to THUMBNAIL_ENLARGE_ALWAYS(1),
	 * source will be enlarged to max output size.
	 * Fill mode is not applicable if no background color assigned.
	 *
	 * @param integer $intEnlargeMode Possible value : THUMBNAIL_ENLARGE_NONE(0) and THUMBNAIL_ENLARGE_ALWAYS(1)
	 * @return void
	 * @access public
	 */
	function set_enlarge_mode ($intEnlargeMode) {
		$this->intEnlargeMode = (int)$intEnlargeMode;
	}
	
	/**
	 * Output thumbnail
	 *
	 * @param string $strDest Path and filename of output file. If not provided, thumbnail will output to browser directly
	 * @return boolean true when done
	 * @access public
	 */
	function output($strDest = NULL) {
		$this->_clear_error();
		if (!empty($this->strSourceFile)) {
			// check the version of ImageMagick
			if (!empty($this->strImageMagicPath)) {
				$arrVersion = $this->_get_im_version();
				if ($arrVersion[1] < 6) $this->strImageMagicPath = NULL; // Disable ImageMagick feature if its version below 6
			}
			
			// obtain source information
			// and check enlarge mode
			$arrSourceInfo = getimagesize($this->strSourceFile);
			
			$img = $this->_gd_output($arrSourceInfo);
			return $img;
		} else {
			return $this->_trigger_error('Invalid source image : '.$this->strSourceFile);
		}
	}
	
//================ Private method
	/**
	 * Output HTTP header
	 *
	 * @return void
	 * @access protected
	 */
	function _send_image_header () {
		header ("Content-Type: " . $this->_output_type2mime());
		header ("Last-Modified: " . gmdate('D, d M Y H:i:s \G\M\T', time()));
		header ("Expires: Thu, 01 Jan 1970 08:00:00 GMT");
		header ("Accept-Ranges: bytes");
		header ('Pragma: no-cache');
		header ('Cache-Control: no-store, no-cache, must-revalidate');
		header ('Cache-Control: pre-check=0, post-check=0, max-age=0', FALSE);
		return;
	}
	
	
	/**
	 * GDlib output method
	 *
	 * @param array $arrSourceInfo Source image infomation including file size and file type
	 * @param string $strCacheImagePath Path and filename of cache file will be saved
	 * @param string $strDest Path and filename for saving the thumbnail. If not provided, thumbnail will output to browser directly
	 * @return boolean true when done
	 * @access protected
	 */
	function _gd_output (&$arrSourceInfo) {
		if (strpos(TP_ALLOWED_IMG_TYPE, $arrSourceInfo['mime'])===FALSE) {
			return $this->_trigger_error('Source file unsupported by GDlib');
		}
		
		// Calculate the total pixel
		// Check for GD limit : total_pixel <= memory_usable/5
		// Fixed : disable memory test when memory_get_usage() is not avaliable.
		$intSourceTotalPixel = $arrSourceInfo[0]*$arrSourceInfo[1];
		
		if (function_exists('memory_get_usage')) {
			$intMemoryAvaliable = $this->_return_bytes(ini_get('memory_limit')) - memory_get_usage();
		} else { // Assume you have enough memory... good luck...
			$intMemoryAvaliable = $intSourceTotalPixel * 6;
		}
		if ($intMemoryAvaliable > $intSourceTotalPixel * 5) {
			switch($arrSourceInfo['mime'])
			{
				case 'image/jpeg' :
				case 'image/pjpeg' :
					$resInputImg = @imagecreatefromjpeg($this->strSourceFile);
					break;
				case 'image/gif' :
					$resInputImg = @imagecreatefromgif($this->strSourceFile);
					break;
				case 'image/png' :
				case 'image/x-png' :
					$resInputImg = @imagecreatefrompng($this->strSourceFile);
					break;
				default :
					return $this->_trigger_error('Invalid output image type');
					break;
			}
			if ($resInputImg) {
				// obtain color's hex value
				if (!empty($this->strBgColor)) {
					$hexFillColor = '0x'.substr($this->strBgColor, 1, 6);
					eval("\$hexFillColor = ".$hexFillColor.";");
				}
				
				// obtain output position and size
				$arrOutputGeometry = $this->_get_output_geometry($arrSourceInfo);
				
				// Place source image on canvas
				if ($arrOutputGeometry['intCanvasWidth'] * $arrOutputGeometry['intCanvasHeight'] * 5 < $intMemoryAvaliable) {
					$resCanvas = imagecreatetruecolor($arrOutputGeometry['intCanvasWidth'], $arrOutputGeometry['intCanvasHeight']);
				} else {
					return $this->_trigger_error('Output image over size. (Max. number of pixel allowed : '.floor($intMemoryAvaliable/5).')');
				}
				if (!empty($this->strBgColor)) imagefill($resCanvas, 0, 0, $hexFillColor);
				imagecopyresampled(
					$resCanvas,
					$resInputImg,
					$arrOutputGeometry['intOutputX'],
					$arrOutputGeometry['intOutputY'],
					0,
					0,
					round($arrOutputGeometry['dblOutputWidth']),
					round($arrOutputGeometry['dblOutputHeight']),
					$arrSourceInfo[0],
					$arrSourceInfo[1]
				);
				
				// output image
				switch ($this->strOutputType) {
					case 'gif' :
						$boolResult = imagegif($resCanvas);
						break;
					case 'png' :
						$boolResult = imagepng($resCanvas);
						break;
					case 'jpg' :
					case 'jpeg' :
					default :
						$boolResult = imagejpeg($resCanvas, NULL , $this->intJPEGQuality);
						break;
				}
				
				if ($boolResult) {
					if (empty($strDest)) { // send cache to output device
						$this->_send_image_header();
						readfile($strCacheImagePath);
					} else { // write cache to file
						copy($strCacheImagePath, $strDest);
					}
					imagedestroy($resInputImg);
					imagedestroy($resCanvas);
					return TRUE;
				} else {
					if ($resInputImg) imagedestroy($resInputImg);
					if ($resCanvas) imagedestroy($resCanvas);
					return $this->_trigger_error('GD export image failed');
				}
			}
		} else { // Over memory limit, throw an exception
			return $this->_trigger_error('Source over size. (Max. number of pixel allowed : '.floor($intMemoryAvaliable/5).')');
		}
	}
	
	/**
	 * Convert image type to mime type string
	 *
	 * @param string $strImageType Image type string. Use native image type when no prarmeter
	 * @return string MIME type
	 * @access protected
	 */
	function _output_type2mime ($strImageType=NULL) {
		if (empty($strImageType)) $strImageType = &$this->strOutputType;
		switch ($strImageType) {
			case 'gif' :
				return 'image/gif';
				break;
			case 'png' :
				return 'image/png';
				break;
			case 'jpg' :
			case 'jpeg' :
			default :
				return 'image/jpeg';
				break;
		}
	}
	
	/**
	 * Convert shorten form file size to byte
	 * Original from PHP.NET
	 *
	 * @param string $strVal Human readable file size string
	 * @return integer Byte value
	 * @access protected
	 */
	function _return_bytes($strVal) {
	   $strVal = str_replace(" ", "", trim($strVal));
	   preg_match("/[a-zA-Z]{1,2}/", $strVal, $match);
	   $unit = (isset($match[0])) ? strtolower($match[0]) : "";
	   $amount = (float)str_replace($unit, "", $strVal);
	   switch($unit) {
	       // The 'G' modifier is available since PHP 5.1.0
	       case 't':
	       case 'tb':
	           $amount *= 1024;
	       case 'g':
	       case 'gb':
	           $amount *= 1024;
	       case 'm':
	       case 'mb':
	           $amount *= 1024;
	       case 'k':
	       case 'kb':
	           $amount *= 1024;
	   }
	
	   return round($amount);
	}
	
  /**
   * get output position and size
   *
   * @param array $arrSourceInfo source image dimension
   * @return array $arrOutputGeometry output file size, coordinate and canvas size
   * @access protected
   */
  function _get_output_geometry (&$arrSourceInfo) {
		$arrOutputGeometry = array();
		if (
			$this->intThumbnailWidth==$arrSourceInfo[0] && 
			$this->intThumbnailHeight==$arrSourceInfo[1]
		) {
			// Source smaller then canvas and no background
			// convert source file to target image type, with cache name
			$arrOutputGeometry['intCanvasWidth'] = $arrSourceInfo[0];
			$arrOutputGeometry['intCanvasHeight'] = $arrSourceInfo[1];
			$arrOutputGeometry['intOutputX'] = 0;
			$arrOutputGeometry['intOutputY'] = 0;
			$arrOutputGeometry['dblOutputWidth'] = $arrSourceInfo[0];
			$arrOutputGeometry['dblOutputHeight'] = $arrSourceInfo[1];
		} else {
			$arrOutputGeometry['intCanvasWidth'] = $this->intThumbnailWidth;
			$arrOutputGeometry['intCanvasHeight'] = $this->intThumbnailHeight;
			$dblWidthRatio = $arrSourceInfo[0] / $arrOutputGeometry['intCanvasWidth'];
			$dblHeightRatio = $arrSourceInfo[1] / $arrOutputGeometry['intCanvasHeight'];
			if (
				(($arrSourceInfo[0] < $arrOutputGeometry['intCanvasWidth']) && ($arrSourceInfo[1] < $arrOutputGeometry['intCanvasHeight']))
			) {
				// place the source file in the middle of canvas
				$arrOutputGeometry['intOutputX'] = round(($arrOutputGeometry['intCanvasWidth'] - $arrSourceInfo[0]) / 2);
				$arrOutputGeometry['intOutputY'] = round(($arrOutputGeometry['intCanvasHeight'] - $arrSourceInfo[1]) / 2);
				$arrOutputGeometry['dblOutputWidth'] = $arrSourceInfo[0];
				$arrOutputGeometry['dblOutputHeight'] = $arrSourceInfo[1];
			} elseif ($dblWidthRatio > $dblHeightRatio) {
				$arrOutputGeometry['intOutputX'] = 0;
				$dblHeightRatio = $arrSourceInfo[1] / $arrSourceInfo[0] * ($arrOutputGeometry['intCanvasWidth'] / $arrOutputGeometry['intCanvasHeight']);
				if (!empty($this->strBgColor)) {
					$arrOutputGeometry['intOutputY'] = round(($arrOutputGeometry['intCanvasHeight'] - ($dblHeightRatio * $arrOutputGeometry['intCanvasHeight'])) / 2);
				} else $arrOutputGeometry['intOutputY'] = 0;
				$arrOutputGeometry['dblOutputWidth'] = $arrOutputGeometry['intCanvasWidth'];
				$arrOutputGeometry['dblOutputHeight'] = $arrOutputGeometry['intCanvasHeight'] * $dblHeightRatio;
			} else {
				$dblWidthRatio = $arrSourceInfo[0] / $arrSourceInfo[1] * ($arrOutputGeometry['intCanvasHeight'] / $arrOutputGeometry['intCanvasWidth']);
				if (!empty($this->strBgColor)) {
					$arrOutputGeometry['intOutputX'] = ceil(($arrOutputGeometry['intCanvasWidth'] - ($dblWidthRatio * $arrOutputGeometry['intCanvasWidth'])) / 2);
				} else $arrOutputGeometry['intOutputX'] = 0;
				$arrOutputGeometry['intOutputY'] = 0;
				$arrOutputGeometry['dblOutputWidth'] = $arrOutputGeometry['intCanvasWidth'] * $dblWidthRatio;
				$arrOutputGeometry['dblOutputHeight'] = $arrOutputGeometry['intCanvasHeight'];
			}
			
			if (empty($this->strBgColor)) {
				$arrOutputGeometry['intCanvasWidth'] = round($arrOutputGeometry['dblOutputWidth']);
				$arrOutputGeometry['intCanvasHeight'] = round($arrOutputGeometry['dblOutputHeight']);
			}
		}
  	return $arrOutputGeometry;
  }
  
	/**
	 * Obtain the version of ImageMagick
	 *
	 * @return array containing 3 elements : [0]-Version string, [1]-Major version, [2]-Subversion, [3]-Minor version
	 * @return boolean false if fail or ImageMagick unavailable
	 * @access protected
	 */
  function _get_im_version () {
  	if (empty($this->strImageMagicPath)) return FALSE;
		exec($this->strImageMagicPath.'convert -version', $arrOutput, $intReturnVar);
		if ($intReturnVar || empty($arrOutput[0])) return FALSE;
		$arrReturn = array();
		preg_match('/^Version:\sImageMagick\s([0-9.]*)\s.*/', $arrOutput[0], $arrMatch);
		$arrReturn[0] = $arrMatch[1];
		$arrVersion = explode('.', $arrMatch[1]);
		$arrReturn[1] = (int)$arrVersion[0];
		$arrReturn[2] = (int)$arrVersion[1];
		$arrReturn[3] = (int)$arrVersion[2];
		return $arrReturn;
  }
  
  
//================ Method for error reporting
	/**
	 * Obtain error status of the class
	 *
	 * @return boolean true if there is any error
	 * @access public
	 */
	function isError () {
		return $this->boolError;
	}
	
	/**
	 * Obtain last error message
	 *
	 * @return string Error message
	 * @access public
	 */
	function get_error_msg () {
		return $this->strErrMsg;
	}
	
	/**
	 * Wrapper for triggering error
	 *
	 * @param  string    $strErrorMsg  Error message
	 * @return boolean   Always return FALSE
	 * @access protected
	 */
	function _trigger_error ($strErrorMsg) {
		$this->boolError = TRUE;
		$this->strErrMsg = $strErrorMsg;
		return FALSE;
	}
	
	/**
	 * Reset error status and error message
	 *
	 * @return void
	 * @access protected
	 */
	function _clear_error () {
		$this->boolError = FALSE;
		$this->strErrMsg = '';
	}
	
	/**
	 * To print some message on picture for debug
	 *
	 * @param resource $resImg The image resource that the message will be placed in.
	 * @param string $strMsg The message
	 * @return void
	 * @access protected
	 */
	function _gd_print_msg (&$resImg, $strMsg) {
		$arrMsg = explode("\n", wordwrap($strMsg,15,"\n",TRUE));
		for ($i=0; $i < count($arrMsg); $i++) {
			imagestring($resImg, 2, 1, $i*12+1, $arrMsg[$i], 0xFFFFFF);
			imagestring($resImg, 2, 0, $i*12, $arrMsg[$i], 0xFF0000);
		}
	}
        
}
?>